﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Documents;
using Startup.Base.Delegates;
using Startup.Base.Exceptions;
using Startup.Project.Algorithm;
using Startup.Project.Classes;
using Startup.Project.Content;
using Startup.Project.Interfaces;
using Startup.Project.Project;
using Startup.Project.Utils;
using Startup.Shell.ModelDesignerPreview;
using ViewBase = Startup.Project.Interfaces.ViewBase;

namespace Startup.Shell.Domain
{
	public class DomainModel
	{
		public UnparameterizedDelegate ProjectChanged;

		private StartupProject _project;

		public StartupProject Project
		{
			get { return _project; }
			private set
			{
				if (_project == value)
					return;

				_project = value;

				if (ProjectChanged != null)
					ProjectChanged();
			}
		}

		public void NewProject()
		{
			StartupProject project = new StartupProject();
			project.Classes.Add(new StartupDomainModelClass());

			Project = project;
		}

		public void OpenProject(string filePath)
		{
			Project = StartupProject.Load<StartupProject>(filePath);
		}

		public void SaveProject()
		{
			if (Project == null)
				throw new IncorrectStateException("The project is null. It cannot be saved.");

			if (Project.HasSourceFile)
			{
				Project.Save();
				return;
			}

			// TODO: implement
			throw new NotImplementedException();
		}

		public void GenerateProject(string projectDir)
		{
			if (string.IsNullOrEmpty(projectDir))
				throw new ArgumentNullException("projectDir");

			if (Project == null)
				throw new IncorrectStateException("The project is null. It cannot be saved.");
			
			// code generation for classes
			var project = new Project.Project.Project("Project");

			ISourceFileContainer domainClasses = project.AddFolder("Domain");
			for (int index = 0; index < Project.Classes.Count; index++)
			{
				var classSource = Project.Classes[index];
				AddClass(classSource, domainClasses, project);
			}

			CustomClass domainClass = AddClass(Project.DomainClass, project, project);
			domainClass.AddConstructor();

			// code generation for views
			SetMainWindow(Project.MainWindow, Project.DomainClass, project);

			project.Save(projectDir);

			Process.Start(Path.Combine(projectDir, "Project.csproj"));
		}
		
		private void SetMainWindow(PreviewContainer mainWindow, StartupClass domainClass, Project.Project.Project project)
		{
			CustomClass domainCustomClass = (CustomClass) project.GetClass(domainClass.Name);
			ClassViewHierarchy rootHierarchy = new ClassViewHierarchy(domainCustomClass, "Root");

			UpdateHierarchy(domainClass, rootHierarchy, mainWindow, project);

			// generate domain content for designer preview
			ClassContent domainClassContent = new ClassContent();
			for (int index = 0; index < domainClass.Properties.Count; index++)
			{
				StartupProperty property = domainClass.Properties[index];
				domainClassContent.Properties.Add(GererateContentFor(property));
			}

			WindowDefinition mainView = AddWindow(project, domainCustomClass, rootHierarchy, domainClassContent);
			project.SetStartWindow(mainView.View);

            // find properies for rendering
//            Guid[] properties = mainWindow.Preview.Children.SelectMany(x => GetRenderedProperties(x)).Distinct().ToArray();
//            List<PropertyPath> publicPropertiesForShellViewModel = properties.Select(x => GetPropertyPath(project, domainClass, x)).ToList();

			// generate domain content for designer preview
//			ClassContent domainClassContent = new ClassContent();
//			for (int index = 0; index < domainClass.Properties.Count; index++)
//			{
//				StartupProperty property = domainClass.Properties[index];
//				domainClassContent.Properties.Add(GererateContentFor(property));
//			}

            // generate window
//			ISourceFileContainer shellHome = project.AddFolder("ShellModule");
//			WindowContainer mainView = shellHome.AddWindow("Shell", (CustomClass)project.GetClass(domainClass.Name), domainClassContent, publicPropertiesForShellViewModel);
//			project.SetStartWindow(mainView);
			
			PreviewContainer firstChild = mainWindow.Preview.Children[0];
			AddUiControls(mainView.View, firstChild, 0, project, rootHierarchy);

			project.GetAllViews().OfType<ViewBase>().ToList().ForEach(x =>
			{
				List<CommandDefinition> commands = CollectCommands(x);
				x.AddCommands(commands);
			});
		}

		private WindowDefinition AddWindow(Project.Project.Project project, CustomClass domainCustomClass, ClassViewHierarchy hierarchy, ClassContent content)
		{
			ISourceFileContainer folder = project.AddFolder("ShellModule");
			WindowDefinition mainShell = folder.AddWindow("Shell", domainCustomClass, content ?? new ClassContent());

			// model
			ConstructorDefinition modelConstructor = mainShell.Model.Constructors.First();
			Field modelBackgroundField = modelConstructor.GetBindedField(modelConstructor.ConstructorParameters.First());

			List<ReadonlyPropertyDefinition> modelPublicProperties = new List<ReadonlyPropertyDefinition>();

			for (int index = 0; index < hierarchy.ViewProperties.Count; index++)
			{
				ClassViewProperty path = hierarchy.ViewProperties[index];

				string name = path.Path.Count > 0 ? path.Path.Last().Name : CSharpNameHelper.GetPropertyName(modelBackgroundField.Name);
				ClassBase type = path.Path.Count > 0 ? path.Path.Last().ReturnedType : modelBackgroundField.Type;

				PropertyDefinition publicProperty = new PropertyDefinition(type, name, false);
				mainShell.Model.AddProperty(publicProperty);

				FieldBase lastField = modelBackgroundField;

				for (int marker = 0; marker < path.Path.Count; marker++)
				{
					CustomAction returnAction = new CustomAction("if ({0} == null) return null;", lastField);
					publicProperty.AddCodeLineToGetter(returnAction);

					ReadonlyPropertyDefinition tempNode = path.Path[marker];

					GetPropertyValueAction getAction = new GetPropertyValueAction(lastField, tempNode);
					publicProperty.AddCodeLineToGetter(getAction);

					lastField = getAction.Result;
				}

				publicProperty.AddCodeLineToGetter(new ReturnAction(lastField));

				StringBuilder builder = new StringBuilder();
				builder.Append(modelBackgroundField.Name);
				path.Path.ForEach(x => builder.AppendFormat(".{0}", x.Name));

				SetFieldValueAction setAction = new SetFieldValueAction(new ThisField(mainShell.Model), new Field(builder.ToString(), type, false, false), new ValueField(mainShell.Model));
				publicProperty.AddCodeLineToSetter(setAction);

				modelPublicProperties.Add(publicProperty);
			}

			// view model
			ConstructorDefinition viewModelConstructor = mainShell.ViewModel.Constructors.First();
			Field viewModelBackgroundField = viewModelConstructor.GetBindedField(viewModelConstructor.ConstructorParameters.First());

			for (int index = 0; index < modelPublicProperties.Count; index++)
			{
				ReadonlyPropertyDefinition publicProperty = modelPublicProperties[index];

				ViewDefinition view = AddView(project, publicProperty.ReturnedType, hierarchy.ViewProperties[index].Target, content == null ? null : content.Properties.FirstOrDefault(x => x.Name == publicProperty.Name));
				
				ReadonlyPropertyDefinition viewModelProperty = new ReadonlyPropertyDefinition(view.ViewModel, view.ViewModel.Name, false);

				GetFieldValueAction getBaseValue = new GetFieldValueAction(mainShell.ViewModel.This, viewModelBackgroundField);
				viewModelProperty.AddCodeLineToGetter(getBaseValue);
				GetPropertyValueAction getTargetValue = new GetPropertyValueAction(getBaseValue.Result, publicProperty);
				viewModelProperty.AddCodeLineToGetter(getTargetValue);
				CreateAction createModelAction = new CreateAction(view.Model.Constructors.First(), getTargetValue.Result);
				viewModelProperty.AddCodeLineToGetter(createModelAction);
				CreateAction createViewModelAction = new CreateAction(view.ViewModel.Constructors.First(), createModelAction.Result);
				viewModelProperty.AddCodeLineToGetter(createViewModelAction);
				ReturnAction returnViewModelAction = new ReturnAction(createViewModelAction.Result);
				viewModelProperty.AddCodeLineToGetter(returnViewModelAction);

				mainShell.ViewModel.AddProperty(viewModelProperty);
			}

			return mainShell;
		}

		private ViewDefinition AddView(Project.Project.Project project, ClassBase domainCustomClass, ClassViewHierarchy hierarchy, ClassContent content)
		{
			string moduleName = domainCustomClass is CollectionStandartClass ? ((CollectionStandartClass)domainCustomClass).TemplateType.Name : domainCustomClass.Name;
			
			ISourceFileContainer folder = project.AddFolder(moduleName + "Module");
			ViewDefinition viewDefinition = folder.AddView(moduleName, domainCustomClass, content ?? new ClassContent());

			// model
			ConstructorDefinition modelConstructor = viewDefinition.Model.Constructors.First();
			Field modelBackgroundField = modelConstructor.GetBindedField(modelConstructor.ConstructorParameters.First());

			List<PropertyDefinition> modelPublicProperties = new List<PropertyDefinition>();

			for (int index = 0; index < hierarchy.ViewProperties.Count; index++)
			{
				ClassViewProperty path = hierarchy.ViewProperties[index];
				
				string name = path.Target.PropertyName;
				ClassBase type = path.Path.Count > 0 ? path.Path.Last().ReturnedType : modelBackgroundField.Type;

				PropertyDefinition publicProperty = new PropertyDefinition(type, name, false);
				viewDefinition.Model.AddProperty(publicProperty);

				FieldBase lastField = modelBackgroundField;

				for (int marker = 0; marker < path.Path.Count; marker++)
				{
					CustomAction returnAction = new CustomAction("if ({0} == null) return null;", lastField);
					publicProperty.AddCodeLineToGetter(returnAction);

					ReadonlyPropertyDefinition tempNode = path.Path[marker];

					GetPropertyValueAction getAction = new GetPropertyValueAction(lastField, tempNode);
					publicProperty.AddCodeLineToGetter(getAction);

					lastField = getAction.Result;
				}

				publicProperty.AddCodeLineToGetter(new ReturnAction(lastField));

				StringBuilder builder = new StringBuilder();
				builder.Append(modelBackgroundField.Name);
				path.Path.ForEach(x => builder.AppendFormat(".{0}", x.Name));

				SetFieldValueAction setAction = new SetFieldValueAction(new ThisField(viewDefinition.Model), new Field(builder.ToString(), type, false, false), new ValueField(viewDefinition.Model));
				publicProperty.AddCodeLineToSetter(setAction);

				modelPublicProperties.Add(publicProperty);
			}

			// view model
			ConstructorDefinition viewModelConstructor = viewDefinition.ViewModel.Constructors.First();
			Field viewModelBackgroundField = viewModelConstructor.GetBindedField(viewModelConstructor.ConstructorParameters.First());

			for (int index = 0; index < modelPublicProperties.Count; index++)
			{
				PropertyDefinition publicProperty = modelPublicProperties[index];
				ClassViewProperty viewProperty = hierarchy.ViewProperties[index];

				bool isFinalProperty = viewProperty.Target.ViewProperties.Count == 0;

				if (isFinalProperty)
				{
					string name = viewProperty.Target.PropertyName;
					ClassBase type = viewProperty.Path.Count > 0 ? viewProperty.Path.Last().ReturnedType : modelBackgroundField.Type;

					PropertyDefinition viewModelProperty = new PropertyDefinition(type, name, false);
					viewDefinition.ViewModel.AddProperty(viewModelProperty);

					GetFieldValueAction getBaseValue = new GetFieldValueAction(viewDefinition.ViewModel.This, viewModelBackgroundField);
					viewModelProperty.AddCodeLineToGetter(getBaseValue);
					GetPropertyValueAction getTargetValue = new GetPropertyValueAction(getBaseValue.Result, publicProperty);
					viewModelProperty.AddCodeLineToGetter(getTargetValue);
					ReturnAction returnViewModelAction = new ReturnAction(getTargetValue.Result);
					viewModelProperty.AddCodeLineToGetter(returnViewModelAction);

					GetFieldValueAction getBaseValue2 = new GetFieldValueAction(viewDefinition.ViewModel.This, viewModelBackgroundField);
					viewModelProperty.AddCodeLineToSetter(getBaseValue2);
					SetPropertyValueAction setTargetValue = new SetPropertyValueAction(getBaseValue2.Result, publicProperty, new ValueField(publicProperty.ReturnedType));
					viewModelProperty.AddCodeLineToSetter(setTargetValue);
				}
				else
				{
					ViewDefinition view = AddView(project, publicProperty.ReturnedType, viewProperty.Target, content == null ? null : content.Properties.FirstOrDefault(x => x.Name == publicProperty.Name));

					ReadonlyPropertyDefinition viewModelProperty = new ReadonlyPropertyDefinition(view.ViewModel, view.ViewModel.Name, false);

					GetFieldValueAction getBaseValue = new GetFieldValueAction(viewDefinition.ViewModel.This, viewModelBackgroundField);
					viewModelProperty.AddCodeLineToGetter(getBaseValue);
					GetPropertyValueAction getTargetValue = new GetPropertyValueAction(getBaseValue.Result, publicProperty);
					viewModelProperty.AddCodeLineToGetter(getTargetValue);
					CreateAction createModelAction = new CreateAction(view.Model.Constructors.First(), getTargetValue.Result);
					viewModelProperty.AddCodeLineToGetter(createModelAction);
					CreateAction createViewModelAction = new CreateAction(view.ViewModel.Constructors.First(), createModelAction.Result);
					viewModelProperty.AddCodeLineToGetter(createViewModelAction);
					ReturnAction returnViewModelAction = new ReturnAction(createViewModelAction.Result);
					viewModelProperty.AddCodeLineToGetter(returnViewModelAction);

					viewDefinition.ViewModel.AddProperty(viewModelProperty);
				}
			}

			return viewDefinition;
		}
		
		private void UpdateHierarchy(StartupClass targetClass, ClassViewHierarchy hierarchy, PreviewContainer previewContainer, Project.Project.Project project)
		{
			if (previewContainer.Preview == null)
				return;
			
			if (previewContainer.Preview is RendererPanelPreview)
			{
				if (targetClass != null)
				{
					foreach (StartupProperty startupProperty in targetClass.Properties)
					{
						List<ReadonlyPropertyDefinition> path = FindProperty(targetClass, startupProperty.Id, project);
						ClassBase newPropertyType = project.GetClass(startupProperty.Type);
						ClassViewHierarchy newHierarchy = new ClassViewHierarchy(newPropertyType, startupProperty.Name);
						ClassViewProperty newProperty = new ClassViewProperty(newHierarchy);
						newProperty.Path.AddRange(path);
						hierarchy.ViewProperties.Add(newProperty);
					}
				}
				else // it's a collection
				{
					List<ReadonlyPropertyDefinition> path = new List<ReadonlyPropertyDefinition>();
					ClassBase newPropertyType = hierarchy.Target;
					ClassViewHierarchy newHierarchy = new ClassViewHierarchy(newPropertyType, hierarchy.PropertyName);
					ClassViewProperty newProperty = new ClassViewProperty(newHierarchy);
					newProperty.Path.AddRange(path);
					hierarchy.ViewProperties.Add(newProperty);
				}
			}

			foreach (PreviewContainer child in previewContainer.Preview.Children)
			{
				if (child.Preview != null && child.Preview is RendererPanelPreview)
				{
					List<ReadonlyPropertyDefinition> path = FindProperty(targetClass, child.DataContextPropertyId, project);

					ClassBase newPropertyType = path.Count > 0 ? path.Last().ReturnedType : hierarchy.Target;

					ClassViewHierarchy newHierarchy = new ClassViewHierarchy(newPropertyType, path.Count > 0 ? path.Last().Name : hierarchy.PropertyName);
					ClassViewProperty newProperty = new ClassViewProperty(newHierarchy);
					newProperty.Path.AddRange(path);

					
					UpdateHierarchy(Project.Classes.FirstOrDefault(x => x.Name == newPropertyType.Name), newHierarchy, child, project);

					hierarchy.ViewProperties.Add(newProperty);
				}
				else
				{
					UpdateHierarchy(targetClass, hierarchy, child, project);
				}
			}
		}

		private List<ReadonlyPropertyDefinition> FindProperty(StartupClass rootStartupClass, Guid propertyId, Project.Project.Project project)
		{
			CustomClass rootCustomClass = (CustomClass)project.GetClass(rootStartupClass.Name);
			StartupProperty property = rootStartupClass.Properties.FirstOrDefault(x => x.Id == propertyId);

			if (property != null)
			{
				ReadonlyPropertyDefinition propertyDefinition = rootCustomClass.Properties.Find(x => x.Name == property.Name);
				return new List<ReadonlyPropertyDefinition>(new[] { propertyDefinition });
			}

			for (int index = 0; index < rootStartupClass.Properties.Count; index++)
			{
				StartupProperty node = rootStartupClass.Properties[index];
				ReadonlyPropertyDefinition propertyDefinition = rootCustomClass.Properties.Find(x => x.Name == node.Name);

				StartupClass startupClass = Project.Classes.FirstOrDefault(x => x.Name == node.Type);
				
				if (startupClass == null)
					continue;
				if (propertyDefinition.ReturnedType.GetType() == typeof(StartupClass))
					continue;

				List<ReadonlyPropertyDefinition> result = FindProperty(startupClass, propertyId, project);

				if (result != null)
				{
					result.Insert(0, propertyDefinition);
					return result;
				}
			}

			return null;
		}

		private static List<CommandDefinition> CollectCommands(UiContainer container)
		{
			List<CommandDefinition> commands = new List<CommandDefinition>();

			if (container is ToolbarContainer)
				commands.AddRange(((ToolbarContainer)container).Commands);

			commands.AddRange(container.Children.Where(x => x != null).SelectMany(x => CollectCommands(x)));

			return commands;
		}

		private PropertyContent GererateContentFor(StartupProperty property)
		{
			PropertyContent content = new PropertyContent { Name = property.Name, Type = property.Type, Value = null };
			StartupClass propertyType = Project.Classes.FirstOrDefault(x => x.Name == property.Type);

			if (property.IsCollection)
			{
				for (int index = 0; index < 20; index++)
				{
					StartupProperty fakeProperty = new StartupProperty();
					fakeProperty.Type = property.Type;

					content.ArrayItems.Add(GererateContentFor(fakeProperty));
				}
			}
			else if (propertyType != null)
			{
				for (int index = 0; index < propertyType.Properties.Count; index++)
				{
					StartupProperty item = propertyType.Properties[index];
					content.Properties.Add(GererateContentFor(item));
				}
			}
			else if (property.DataContract != null)
			{
				content.Value = property.DataContract.Generate().ToString();
			}

			return content;
		}

        private PropertyPath GetPropertyPath(Project.Project.Project project, StartupClass startupClass, Guid propertyId)
	    {
            if (propertyId == Guid.Empty)
                return null;

            StartupProperty property = startupClass.FindProperty(propertyId);

            if (property != null)
            {
                PropertyPath result = new PropertyPath { Type = project.GetClass(property.Type), Name = property.Name };
                result.Add(property.Name);

                return result;
            }

            for (int index = 0; index < startupClass.Properties.Count; index++)
            {
                StartupProperty parentProperty = startupClass.Properties[index];
                StartupClass parentPropertyType = Project.Classes.FirstOrDefault(x => x.Name == parentProperty.Type);

                if (parentPropertyType == null)
                    throw new Exception(string.Format("Class '{0}' has not been found.", parentProperty.Name));

                PropertyPath path = GetPropertyPath(project, parentPropertyType, propertyId);

                if (path.Count > 0)
                {
                    path.Insert(0, parentProperty.Name);
                    return path;
                }
            }

            throw new IncorrectStateException("There is an unknown state of algorithm.");
	    }

	    private static Guid[] GetRenderedProperties(PreviewContainer previewContainer)
	    {
            if (previewContainer.DataContextPropertyId != Guid.Empty)
                return new[] { previewContainer.DataContextPropertyId };

            if (previewContainer.DataContextPropertyId == Guid.Empty && previewContainer.Preview == null)
                return new[] { Guid.Empty };

	        return previewContainer.Preview.Children.SelectMany(x => GetRenderedProperties(x)).ToArray();
	    }

	    private void AddUiControls(UiContainer uiContainer, PreviewContainer child, int index, Project.Project.Project project, ClassViewHierarchy hierarchy)
		{
		    if (child.Preview == null) 
                return;

            // DataContext calcutation
            StartupProperty property = Project.FindProperty(child.DataContextPropertyId);
            ClassBase dataContextType = 
				child.DataContextPropertyId == Guid.Empty ? project.GetClass(Project.DomainClass.Name) :
				property.IsCollection ? new CollectionStandartClass(project.GetClass(property.Type)) : project.GetClass(property.Type);
			
            // UI generation
            UiContainer childContainer = child.Preview.GetContainer(dataContextType, property);
		    uiContainer.AddChild(childContainer, index);
				
		    for (int marker = 0; marker < child.Preview.Children.Length; marker++)
		    {
		    	PreviewContainer newChild = child.Preview.Children[marker];
				
				if (newChild.Preview == null)
					continue;

				if (newChild.DataContextPropertyId == Guid.Empty)
				{
					AddUiControls(childContainer, newChild, marker, project, hierarchy);
					return;
				}

				StartupProperty newChildProperty = Project.FindProperty(newChild.DataContextPropertyId);
				string typeName = newChildProperty.IsCollection ? string.Format("ObservableCollection<{0}>", newChildProperty.Type) : newChildProperty.Type;
				ClassViewProperty newChildView = hierarchy.ViewProperties.FirstOrDefault(x => x.Path.Last().ReturnedType.Name == typeName);

				if (newChildView != null && newChild.Preview is RendererPanelPreview)
				{
					ViewBase view = project.GetView(newChildProperty.Type + "View");
					AddUiControls(view, newChild, 0, project, newChildView.Target);

					childContainer.AddChild(new ViewLinkContainer(view), marker);
				}
				else 
				{
					AddUiControls(childContainer, newChild, marker, project, hierarchy);
				}
		    }
		}

		private static CustomClass AddClass(StartupClass source, ISourceFileContainer container, Project.Project.Project project)
		{
			var generatedClass = container.AddClass(source.Name);

			for (int index = 0; index < source.Properties.Count; index++)
			{
				StartupProperty propertySource = source.Properties[index];
				bool isCollection = propertySource.IsCollection;
				string propertyName = propertySource.Name;
				ClassBase propertyType = project.GetClass(propertySource.Type);

				if (isCollection)
					propertyType = new CollectionStandartClass(propertyType);

				generatedClass.AddAutoProperty(propertyName, propertyType);
			}

			return generatedClass;
		}
	}
}
